﻿using System;
using System.Diagnostics;
using ProtoBuf;

namespace GeoAzure.Core
{
    /// <summary>
    /// Represents a point on the earth using its longitude and latitude
    /// </summary>
    [Serializable]
    [ProtoContract]
    public struct Location : IEquatable<Location>
    {
        public const double MaxLatitude = Math.PI / 2d;
        public const double MinLatitude = -Math.PI / 2d;
        public const double MaxLongitude = Math.PI;
        public const double MinLongitude = -Math.PI;

        [ProtoMember(1)]
        private readonly double _latitude;
        [ProtoMember(2)]
        private readonly double _longitude;

        #region Construction

        public static Location FromDegrees(double latitude, double longitude)
        {
            return new Location(Utils.ToRadians(latitude), Utils.ToRadians(longitude));
        }

        public Location(double latitude, double longitude)
        {
            Debug.Assert(latitude.IsBetween(MinLatitude, MaxLatitude));
            Debug.Assert(longitude.IsBetween(MinLongitude, MaxLongitude));
            _longitude = longitude;
            _latitude = latitude;
        }

        #endregion

        /// <summary>
        /// The latitude in radians.
        /// </summary>
        public double Latitude
        {
            get { return _latitude; }
        }

        /// <summary>
        /// The longitude in radians.
        /// </summary>
        public double Longitude
        {
            get { return _longitude; }
        }

        public bool IsPole
        {
            get { return Latitude == MaxLatitude || Latitude == MinLatitude; }
        }

        #region Equality members

        public bool Equals(Location other)
        {
            bool areLatitudesEqual = other._latitude.Equals(_latitude);
            if (IsPole)
                return areLatitudesEqual;
            return areLatitudesEqual && other._longitude.Equals(_longitude);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            if (obj.GetType() != typeof(Location))
                return false;
            return Equals((Location) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (_longitude.GetHashCode() * 397) ^ _latitude.GetHashCode();
            }
        }

        public static bool operator ==(Location left, Location right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Location left, Location right)
        {
            return !left.Equals(right);
        }

        #endregion

        public double DistanceTo(Location other)
        {
            // this uses the Haversine formula
            // see http://en.wikipedia.org/wiki/Haversine_formula for an explanation
            // and http://www.movable-type.co.uk/scripts/latlong.html for an example implementation used hereunder
            var deltaLat = Latitude - other.Latitude;
            var deltaLong = Longitude - other.Longitude;

            var sinDeltaLat2 = Math.Sin(deltaLat / 2d);
            var sinDeltaLong2 = Math.Sin(deltaLong / 2d);
            var a = sinDeltaLat2 * sinDeltaLat2 + sinDeltaLong2 * sinDeltaLong2 * Math.Cos(Latitude) * Math.Cos(other.Latitude);
            var c = 2d * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1d - a));
            return Constants.EarthRadius * c;
        }
    }

    public static class Extensions
    {
        public static bool IsBetween(this double value, double low, double high)
        {
            return value <= high && value >= low;
        }
    }
}
